Effective Python

函数

第14条:尽量用异常来表示特殊情况,而不要反悔None

  1. 用None这个返回值来表示特殊意义的函数,很容易犯错,因为0和空字符串在条件表达式当中也会被评估为False
  2. 返回None的时候想想正常返回会不会有0或者空字符串

第15条:了解如何在闭包里使用使用外围作用域的变量

  1. 在表达式中引用变量的时候,python解释器将按如下顺序遍历各作用域,以解析该应用:
(1)当前函数的作用域
(2)任何外围作用域
(3)包含当前代码的那个模块的作用域
(4)内置作用域

而在变量赋值的时候,如果当前作用域没有这个变量,则会直接把此次赋值视为对该变量的定义

  1. nonlocal无法延伸到模块级别,这是为了防止其污染全局作用域
  2. 对于复杂的情况,不要使用闭包,而是应当使用类来存储状态

第16条:考虑用生成器来改写直接返回列表的函数

第17条:在参数上面迭代时要多加小心

函数在输入的参数上面多次迭代时要当心,如果迭代对象是迭代器,那么可能会导致值失效,最好的解决方法再里面判断一下是不是迭代器,如果是的话把它转化成容器类型,防止迭代器的多次迭代

第18条:用数量可变的位置参数减少视讯误差

第19条:用关键字参数来表示可选行为

  1. 关键字参数的好处在于可以进行简单的扩充而不给程序带来bug
  2. 函数的参数默认值会在模块加载的时候直接求出,所以不能简单地对函数参数设定动态默认值

第20条:用None和文档字符串来描述具有动态默认值的参数

要动态,可以设定为None,然后再文档字符串中说明一下,函数内部做判断,如果为None则动态生成

第21条:用只能以关键词形式指定的参数来确保代码明晰

类与集成

第22条:尽量用辅助类来维护程序的状态,而不要用字典和元组

  1. 所谓动态是指待保存的这些信息其标识符无法提前获知
  2. 针对复杂的关系,使用字典和元组就不太合适了,比如当你需要使用到嵌套字典的时候,说明是时候去构造一个辅助类了
  3. 元组里的元素一旦超过两项,就可以尝试使用其他方法了,一个可用于替代的方案是namedtuple
  4. namedtuple的局限在于无法指定各参数的默认值,对于可选属性比较多的数据来说还是定义自己的类比较好
  5. 但是namedtuple很容易重构为完整的类

第23条:简单的接口应该接受函数,而不是类的实例

  1. 用函数来作为hook优先级更高,因为其有明确的参数和返回值,这是函数能作为一等对象的语言的一大优势
  2. hook如果需要保存状态可以使用闭包函数,但更好的选择是callalbe的类实例

第24条:以@classmethod形式的多态去通用地构建对象

  1. 多态是指继承体系中的多各类都能以个字所独有的方法来实现某个方法,这些类都满足相同的接口或继承相同的抽象类,但却又不同的功能。(又复习了一下多态,但是python是动态类型,也不存在向上转型什么的)
  2. 通过@classmethod可以用一种与构造方法相似的方式来构造类的对象
  3. 剩下的关于多态的我真的是搞不懂了

第25条:用super初始化父类

多重继承的话要注意超类的调用顺序,不过多重继承应当只继承一个实体类,其他都是混类或者抽象类

第26条:只在使用Mix-in组件制作工具类时进行多重继承

对我来说最大的问题就是,什么时候用mix-in

第27条:多用public属性,少用private属性

  1. 归根结底是使用private属性之后子类在覆写或扩展的时候会遇到麻烦和错误(具体是什么样的麻烦和错误呢?)
  2. 运用protected属性时要指明哪些属性是内部自用的,哪些属性不应当被修改的这些建议

第28条:继承collections.abc以实现自定义的容器类型

  1. 容器就是封装了属性与功能的意思、
  2. 简单的子类可以继承collections模块当中那些让用户来继承的类,比如UserDict,UserList这种
  3. 如果要改写特殊方法,则去继承抽象基类而非内置类型,不然你编写的特殊方法会无效掉

元类及属性

第29条:用纯属性取代get和set方法

编写新类的时候,应该用简单的public属性来定义其接口,而不要去实现set和get方法,之后再使用@property进行拓展

第30条:考虑用@property来代替属性重构

@property的好处是可以简单地为现有的实例属性添加新的功能

第31条:用描述符来改写需要服用的@property方法

@property装饰器的一个问题在于其修饰的这些方法,无法为同一个类的其他属性所服用,对于这个问题可以采用自定义的descriptor来解决

由于描述符必须定义在类当中才有效,所以你创建的所有类实例中所对象的描述符对象都是同一个描述符对象,这样的话如果你需要在描述符对象当中进行一些状态存储,必须使用一种针对实例的存储方式,这样可以使用实例作为键进行数据的读取

但是上述的解决方案会有内存泄露的问题,因为所有的实例引用都会被保存,可以使用WeakKeyDictionary进行解决,不过暂时还是不去深究会比较好

第32条:用__getattr__等实现按需生成的属性

  1. 通过__getattr__和__setattr__可以用惰性的方式来加载并保存对象的属性
  2. 如果要在__getattribute__和__setattr__方法中访问实例属性,那么应该直接通过super来做,避免无限递归(也就是object类的同名方法,object类是没有__getattr__的)

第32-35条:元类相关

元类相关的先略过

第36条:用subprocess模块来管理子进程

稍微了解一下,暂时还用不到

第37条:可以用线程来执行阻塞式I/O,但不要用它做平行计算

  1. GIL就是保证多个线程在同一时刻不会修改同一份数据
  2. python在有GIL的情况下,还使用多线程的理由:
(1)可以使程序看上去好像能够在同一时间做许多事情
(2)处理阻塞式I/O操作

第38条:在线程中使用Lock来防止数据竞争

GIL并不会保护开发者自己所编写的代码,当线程正在操作某个数据结构的时候,其他线程可能会打断它

../_images/WX20170909-163752@2x.png

使用互斥锁能够保证在执行代码段的时候,别的需要该锁的代码段执行不了

第39条:用Queue来协调各线程之间的工作

  1. 管线是一种优秀的任务处理方式,它可以把处理流程划分为若干阶段,并使用多条python线程来同时执行这些任务
  2. 构建并发式的管线的时候,要注意许多问题,其中包括:如何防止某个阶段陷入持续等待的状态之中、如何停止工作线程,以及如何防止内存膨胀等
  3. Queue类所提供的机制,可以彻底解决上述问题,其具备阻塞式的队列操作、能够指定缓冲区尺寸,而且还支持join方法,这使得开发者可以构建出健壮的管线

第40条:考虑用协程来并发地运行多个函数

  1. 协程的好处在于开销小
  2. 对于生成器内的yield表达式来说,外部代码通过send方法传给生成器的那个值,就是该表达式所要具备的值,调用next()就相当于send(None)
  3. 例子懒得仔细看了,以后复习的时候看一下

第41条:考虑用concurrent.futures来实现真正的平行计算

简单地说就是计算密集型的代码,用concurrent.futures的多进程来实现

内置模块

第42条:用functools.wraps定义函数装饰器

之所以要使用functools.wraps是因为普通的装饰器会导致需要使用内省机制的工具无法正常工作

wraps的作为其实就是将内部函数相关的重要元数据复制到外围函数(wrapper)

第43条:考虑以contextlib和with语句来改写可复用的try/finally代码

对于contextmanager当中为什么要用try还是不太清楚,需要复习一下fluent python,最好有空自己实现一下,测试一下

第44条:用copyreg实现可靠的pickle操作

  1. 用pickle模块序列化的数据,采用的是一种不安全的格式,序列化后的数据实际上就是一个程序,描述了如何来构建原始的Pyhton对象,而一旦混入恶意数据反序列化的话就有可能对程序造成损害。相反,json是一种安全的格式,反序列化不会出现任何问题。
  2. pickle使用方便,但是某些情况下如反序列化前改变的类就会出现意想不到的问题
  3. 对于上文所说的这种情况,可以使用copyreg模块注册一些函数,来进行解决
  4. 大致了解一下,具体的以后需要用到的时候再学习,不然学起来费劲,学会了也容易忘记

第45条:用datetime模块来处理本地时间,而不是time模块

主要涉及到时区转换,暂时先不了解

第46条:使用内置算法与数据结构

  1. 双向队列:deque,可以从头部或尾部插入或移除一个元素,只需要消耗常数级别的时间,也就是和队列长度无关,但从list头部插入却会耗费线性级别的时间
  2. 有序字典:OrderedDict,会按照键的插入顺序来保留键值对在字典中的次序,因此可以保证在其之上根据键来迭代器行为是确定的,而dict则是不确定的,这是因为哈希算法的缘故,排列是按照偏移量来排的,一旦出现哈希冲突就会导致不确定(不过实验了一下,新版本的python当中好像是确定的,但是用python2.7就可以获得实验结果,还有一个不太理解的点,是否是哈希冲突导致的呢?)
  3. 带有默认值的字典:defaultdict
  4. 堆队列:优先级队列,直接用list加上heapq模块实现,感觉和一般的堆不太一样,不知道有什么作用
  5. 二分查找:使用bisect.bisect_left实现对列表的二分查找,复杂度为对数级别
  6. 迭代器工具:itertools模块当中

第47条:在重视精确度的场合,应该使用decimal

略过

第48条:学会安装由Python开发者社区所构建的模块

使用pip进行安装即可

协作开发

第49条:为每个函数、类和模块编写文档字符串

  1. 模块的docstring应该作为源文件的第一条语句,头一行应该是一句话,用以描述模块的用途,下面的那段话,应该包含一些细节信息,主要是与模块的操作相关的
  2. 类的docstring与模块级的docstring大致相同,但是要介绍pubic属性及方法,以及还应该告诉子类实现者,如何使用protected属性及方法

范例

../_images/WX20170911-160357@2x.png
  1. 函数的docstring要注意协程、生成器、默认值参数的情况,并根据不同情况加以说明,这些可以写入我的代码风格当中

第50条:用包来安排模块,并提供稳固的API

  1. 包内的相对引用,要写清楚上级模块的绝对名称,不能直接只有一个.( 为什么 ),这一点我需要改进一下
  2. 从不同包中引用同名模块可以使用as重新起名防止冲突
  3. 为包或模块编写__all__属性可以仅提供给外部你所想要提供的属性,这个基本用不上,明白怎么一回事就行了

第51条:为自编的模块定义根异常,一遍将调用者与API相隔离

根异常继承Exception即可

通过捕获根异常,调用者可以得知他们在使用你的API的时候,所编写的调用代码是否正确。

意思好像定义了根异常就不该抛出标准异常,这样有必要么?

第52条:用适当的方式打破循环依赖关系

../_images/WX20170912-092918@2x.png

解决循环依赖的三种方式:

(1)调整引入顺序
(2)先引入、再配置、最后运行
(3)动态引入
../_images/WX20170914-155833@2x.png

理解一下为什么会出现这个异常,因为在main里面import app后会先将空模块加载仅sys.modules,然后再执行模块内的代码,所以会导致dialog.py里面import app时发现app里面是空的,这就是循环引用导致的问题所在。要解决这个问题可以等要被引用模块引用的变量定义了以后再引用引用模块。

然而经过实验直接运行app的话不会出现这个问题,直接运行和import有什么不一样呢?噢噢,我明白了,直接运行的时候app并没有被载入sys.modules,因此dialog import app的时候会import app,而app进入第五步运行代码的时候import dialog,此时sys.modules已经有了dialog,因此就import了一个空的dialog,由于app中没有用到dialog,因此不会报错。

那么推测app中如果用了dialog,肯定会报错,因为此时dialog未被加载。实验了一下:果然是这样。

这样来看,为什么要先在模块中加载再运行呢?其实是为了防止无限递归。

附代码

a.py

import b

a = 3

# 加入这行直接运行代码a.py就会报错
print(b.b)

if __name__ == '__main__':
    print(b.b)

b.py

import a

b = 999 + a.a

第53条:用虚拟环境隔离项目,并重建其依赖关系

略过

第54条:考虑用模块级别的代码来配置不同的部署环境

感觉这里的方法并不是很好,直接使用环境变量不就完事了

第55条:通过repr字符串输出调试信息

针对内置的Python类型来调用repr函数,会根据该值返回一条可供打印的字符串,把这个字符串传给内置的eval函数,就可以将其还原为最初的那个值

第56条:用unittest来测试全部代码

略过

第57条:考虑用pdb实现交互调试

略过

第58条:先分析性能,然后再优化

可以尝试使用cProfile做性能分析,有助于我理解python更底层的机制

第59条:用tracemalloc来掌握内存的使用及泄露情况

  1. 可以使用gc模块查询垃圾收集器当前所致的每个对象
  2. tracemalloc可以用来打印对象的完整堆栈信息,这个东西很有用啊